import argparse
import glob
import os
import re
import subprocess
import sys

import distutils.version


class Package(object):
    """
    rpm package
    """

    def __init__(self, file_name):
        """
        init function
        """
        package_arch = file_name.split('.')[-2]
        package_name_version = re.sub(package_arch + '.rpm', '', file_name)
        package_version = re.findall(r'-\d+.*$', package_name_version)[0]
        package_name = re.sub(package_version, '', package_name_version)
        package_version = re.sub(r'^-', '', package_version)
        package_version = re.sub(r'\.+$', '', package_version)
        self.file_name = file_name
        self.name = package_name
        self.version = package_version
        self.arch = package_arch

    def __str__(self):
        """
        string of Package
        """
        return 'Name: ' + self.name + ', Version: ' + self.version + \
            ', Arch: ' + self.arch


class ABIChecker:
    def __init__(self):
        self.pkg_name = ""
        self.folder = ""
        self.common_dir = ""
        self.version_dirs = list()
        self.dumps = list()
        self.get_so = False
        self.so_and_dump = False
        self.get_dump = False
        self.dump_and_check = False
        self.do_check = False

    @staticmethod
    def get_version_num(packages):
        """
        get the version of given packages
        """
        version_num = []
        for package in packages:
            if package.version not in version_num:
                version_num.append(package.version)
        if distutils.version.LooseVersion(version_num[0]) > distutils.version.LooseVersion(version_num[1]):
            version_num[0], version_num[1] = version_num[1], version_num[0]
        return version_num

    @staticmethod
    def rpm_uncompress(packages, common_dir, version_num):
        """
        uncompress the rpm packages in common_dir
        """
        os.chdir(common_dir)
        for version in version_num:
            if not os.path.exists(version):
                os.mkdir(version)
        for package in packages:
            os.chdir(common_dir)
            os.chdir(package.version)
            os.system(f"rpm2cpio {common_dir}/{package.file_name} | cpio -div")

    @staticmethod
    def get_so_file_name(so_file_name):
        """
        get the .so file name
        ie: foo.so.0.0 -> foo.so
        """
        ret = re.findall(r'.*.so', so_file_name)
        if ret:
            return ret[0]
        else:
            return ""

    @staticmethod
    def dumper_by_debuginfo(ver_dir, version):
        """
        generate .dump file by debuginfo .rpm file
        """
        os.chdir(ver_dir)
        so_files = []
        for root, dirs, files in os.walk(ver_dir):
            for file in files:
                full_file = os.path.join(root, file)
                if re.match(r'.*\.so.*', full_file) and not os.path.islink(full_file):
                    so_files.append(full_file)
        res = []
        if len(so_files) > 0:
            for so_file in so_files:
                _, so_file_name = os.path.split(so_file)
                so_file_name = ABIChecker.get_so_file_name(so_file_name)
                if so_file_name and os.system(
                        f"abi-dumper {so_file} --search-debuginfo={ver_dir}/usr/lib/debug/ -o ABI-{so_file_name}.dump"
                        f" -lver {version}") == 0:
                    res.append(so_file_name)
        return res

    @staticmethod
    def do_abi_compliance_check(common_dir, old_dump_abi, new_dump_abi, old_version, new_version):
        """
        abi compliance check
        """
        if not os.path.exists("compat_reports"):
            os.makedirs("compat_reports")
        # 指定输出文件
        output_file_path = 'compat_reports/abi_compliance_report.txt'
        old_dump_path = os.path.join(common_dir, old_version, 'ABI-' + old_dump_abi + '.dump')
        new_dump_path = os.path.join(common_dir, new_version, 'ABI-' + new_dump_abi + '.dump')
        with open(output_file_path, 'a') as f:
            process = subprocess.Popen(
                ['abi-compliance-checker', '-l', old_dump_abi, '-old', old_dump_path, '-new', new_dump_path],
                stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)

            while True:
                output = process.stdout.readline()
                error = process.stderr.readline()

                if not output and not error:
                    break

                if output:
                    f.write(output)
                    print(output, end='')

                if error:
                    print(error, end='')
            process.wait()

    @staticmethod
    def generate_compatibility_html_report(report_file_path, output_html_path):
        """
        Generate an HTML report based on compatibility data.

        Parameters:
        - report_file_path (str): The path to the compatibility report file (e.g., "abi_compliance_report.txt").
        - output_html_path (str): The path to the output HTML file that will be generated.

        Generates an HTML report table with information extracted from the compatibility report file.
        The HTML table includes details about libraries, binary compatibility, source compatibility,
        total binary compatibility problems, total source compatibility problems, and a link to the detailed report.

        The generated HTML file is saved to the specified output path.

        Usage example:
        generate_compatibility_html_report("abi_compliance_report.txt", "compatibility_reports.html")
        """
        html_template = """
        <!DOCTYPE html>
        <html>
        <head>
            <style>
                table {{
                    border-collapse: collapse;
                    width: 100%;
                }}
                th, td {{
                    border: 1px solid #dddddd;
                    text-align: left;
                    padding: 8px;
                }}
                th {{
                    background-color: #f2f2f2;
                }}
                a {{
                    color: blue;
                    text-decoration: underline;
                    cursor: pointer;
                }}
                .darkorange {{
                color: darkorange;
                }}
                .red {{
                    color: red;
                }}
                .green {{
                    color: green;
                }}
            </style>
        </head>
        <body>

        <h2>兼容性报告总览</h2>

        <table>
            <tr>
                <th>序号</th>
                <th>动态库</th>
                <th>二进制兼容性</th>
                <th>源码兼容性</th>
                <th>二进制兼容性问题</th>
                <th>源码兼容性问题</th>
                <th>详情</th>
            </tr>
            {rows}
        </table>

        </body>
        </html>
        """
        # Extracting data from the abi_compliance_report.txt content
        reports = []
        with open(report_file_path, "r") as report_file:
            lines = report_file.readlines()
            for index, i in enumerate(range(0, len(lines), 9), start=1):  # Assuming each report has 9 lines
                report_info = lines[i:i + 9]
                binary_compatibility = report_info[4].split(": ")[1].strip()
                source_compatibility = report_info[5].split(": ")[1].strip()
                # 根据百分比添加样式类
                binary_compatibility_percentage = float(binary_compatibility.rstrip('%'))
                source_compatibility_percentage = float(source_compatibility.rstrip('%'))
                binary_compatibility_class = "green" if binary_compatibility_percentage == 100 else "red" if binary_compatibility_percentage == 0 else "darkorange"
                source_compatibility_class = "green" if source_compatibility_percentage == 100 else "red" if source_compatibility_percentage == 0 else "darkorange"

                match = re.search(r'problems: \d+, warnings: \d+', report_info[6])
                if match:
                    total_binary_problems = match.group(0)
                else:
                    total_binary_problems = "未知"
                match = re.search(r'problems: \d+, warnings: \d+', report_info[7])
                if match:
                    total_source_problems = match.group(0)
                else:
                    total_source_problems = "未知"
                report_link = report_info[8].split(": ")[1].strip()
                match = re.search(r'compat_reports/([^/]+)/', report_link)
                if match:
                    library = match.group(1)
                else:
                    library = "NaN"
                report_link = report_link.replace("compat_reports/", "")
                row = f"""
                <tr>
                    <td>{index}</td>
                    <td>{library}</td>
                    <td class="{binary_compatibility_class}">{binary_compatibility}</td>
                    <td class="{source_compatibility_class}">{source_compatibility}</td>
                    <td>{total_binary_problems}</td>
                    <td>{total_source_problems}</td>
                    <td><a href="{report_link}" target="_blank">报告</a></td>
                </tr>
                """
                reports.append(row)

        # Create HTML file with the formatted data
        html_content = html_template.format(rows="\n".join(reports))
        with open(output_html_path, "w") as html_file:
            html_file.write(html_content)

    @staticmethod
    def abi_compliance_check(common_dir, dumps, ver_dirs):
        """
        check the compatibility by given .dump files in common_dir + ver_dirs
        """
        old_dump = dumps[0]
        new_dump = dumps[1]
        if len(old_dump) == 0:
            print('Error: Did not find .so file in old_version')
            return
        if len(new_dump) == 0:
            print('Error: Did not find .so file in new_version')
            return
        os.chdir(common_dir)
        old_dump_set = set(old_dump)
        new_dump_set = set(new_dump)
        intersection_dump = old_dump_set & new_dump_set
        unique_new_dump = new_dump_set - old_dump_set
        if unique_new_dump:
            print('Warning: old_version has less .so files, try checking these .so files.')
            for unique_item in unique_new_dump:
                print(unique_item)
            while True:
                user_input = input("是否要继续？(y/n): ").lower()
                if user_input == 'y':
                    break
                elif user_input == 'n':
                    sys.exit()
                else:
                    print("无效的输入，请输入 'y' 或 'n'.")
        unique_old_dump = old_dump_set - new_dump_set
        if unique_old_dump:
            print('Warning: new_version has less .so files, try checking these .so files.')
            for unique_item in unique_old_dump:
                print(unique_item)
            while True:
                user_input = input("是否要继续？(y/n): ").lower()
                if user_input == 'y':
                    break
                elif user_input == 'n':
                    sys.exit()
                else:
                    print("无效的输入，请输入 'y' 或 'n'.")
        for dump in intersection_dump:
            ABIChecker.do_abi_compliance_check(common_dir, dump, dump, ver_dirs[0], ver_dirs[1])
        ABIChecker.generate_compatibility_html_report("compat_reports/abi_compliance_report.txt",
                                                      "compat_reports/compatibility_reports.html")

    def parse_args(self):
        """
        参数解析
        :return:
        """
        parse = argparse.ArgumentParser(description='ABI Checker')
        parse.add_argument('pkg_name', type=str, help='目录名')
        parse.add_argument('folder', type=str, help='路径')
        group = parse.add_mutually_exclusive_group(required=True)
        group.add_argument('--get-so', action='store_true', help='只解压rpm获取so')
        group.add_argument('--so-and-dump', action='store_true', help='解压rpm并生成dump文件')
        group.add_argument('--get-dump', action='store_true', help='只生成dump文件')
        group.add_argument('--dump-and-check', action='store_true', help='生成dump文件并执行对比')
        group.add_argument('--do-check', action='store_true', help='只执行对比操作')

        param = parse.parse_args()
        self.pkg_name = param.pkg_name
        self.folder = param.folder
        self.common_dir = os.path.join(self.folder, self.pkg_name)
        self.get_so = param.get_so
        self.so_and_dump = param.so_and_dump
        self.get_dump = param.get_dump
        self.dump_and_check = param.dump_and_check
        self.do_check = param.do_check

    def get_dumps(self):
        """
        获取需要对比的so文件列表
        :return:
        """
        dumps = list()
        for version in self.version_dirs:
            tmp_list = list()
            file_path = os.path.join(self.common_dir, version)
            dumps_path = glob.glob(f"{file_path}/ABI-*.dump")
            for dump in dumps_path:
                _, file_name = os.path.split(dump)
                so_file = re.search(r"ABI-(.*)\.dump", file_name)
                if not so_file:
                    continue
                so_file = so_file.group(1)
                tmp_list.append(so_file)
            dumps.append(tmp_list)
        return dumps

    def step_rpm_uncompress(self):
        """
        解压rmp步骤
        :return:
        """
        files = os.listdir(self.common_dir)
        rpm_files = [file for file in files if file.endswith('.rpm')]
        packages = [Package(rpm) for rpm in rpm_files]
        ver_dirs = ABIChecker.get_version_num(packages)
        if len(ver_dirs) != 2:
            print('The version to be checked is not 2.')
            sys.exit(-1)
        ABIChecker.rpm_uncompress(packages, self.common_dir, ver_dirs)
        self.version_dirs = ver_dirs

    def step_build_dump(self):
        """
        生成dump文件步骤
        :return:
        """
        if not self.version_dirs:
            abs_path = os.path.abspath(self.common_dir)
            self.version_dirs = [file for file in os.listdir(abs_path) if os.path.isdir(f"{abs_path}/{file}")]
        if len(self.version_dirs) != 2:
            print('The version to be checked is not 2.')
            sys.exit(-1)
        dumps = list()
        for ver_dir in self.version_dirs:
            dumps.append(ABIChecker.dumper_by_debuginfo(f"{self.common_dir}/{ver_dir}", ver_dir))
        self.dumps = dumps

    def step_compliance_check(self):
        """
        dump 对比步骤
        :return:
        """
        if not self.version_dirs:
            abs_path = os.path.abspath(self.common_dir)
            self.version_dirs = [file for file in os.listdir(abs_path) if os.path.isdir(f"{abs_path}/{file}")]
        if len(self.version_dirs) != 2:
            print('The version to be checked is not 2.')
            sys.exit(-1)
        if not self.dumps:
            self.dumps = self.get_dumps()
        ABIChecker.abi_compliance_check(self.common_dir, self.dumps, self.version_dirs)

    def main(self):
        """
        方法调度
        :return:
        """
        if not any((self.get_dump, self.dump_and_check, self.do_check)):
            self.step_rpm_uncompress()
        if not any((self.do_check, self.get_so)):
            self.step_build_dump()
        if not any((self.get_so, self.so_and_dump, self.get_dump)):
            self.step_compliance_check()


if __name__ == '__main__':
    t = ABIChecker()
    t.parse_args()
    t.main()
